在操作外部資源時,會造成副作用
,例如新增一筆資料到資料庫
,就是一個包含副作用的操作。
實務上有個場景還滿常見,我們常常需要操作多個外部資源,只要其中一個環節失敗,我們就要對前面已經造成的副作用
的操作做復原
。手動做這些操作其實有點繁瑣惱人,所以我們 FP 工具 effect 也提供了解決這類問題的框架。
以課程系統來舉例,這次我們想連通三種不同類型的資料服務,三個服務儲存的資料分別是課程的附檔文件或影片 (attachment)、新增課程的 Log (record),以及課程相關資訊 (meta)。當任意一個操作失敗時,都要對前面的操作做復原,所以我們所建立的服務除了提供新增功能以外,還要提供刪除功能。
export interface MinIo {
createAttachment: Effect.Effect<never, MinIoError, Attachment>
deleteAttachment: (
attachment: Attachment
) => Effect.Effect<never, never, void>
}
const MinIo = { context: Context.Tag<MinIo>() }
export interface Elastic {
createCourseRecord: (
attachment: Attachment
) => Effect.Effect<never, ElasticError, CourseRecord>
deleteCourseRecord: (
record: CourseRecord
) => Effect.Effect<never, never, void>
}
const Elastic = { context: Context.Tag<Elastic>() }
export interface Mongo {
createCourseMeta: (
record: CourseRecord
) => Effect.Effect<never, MongoError, Course>
deleteCourseMeta: (course: Course) => Effect.Effect<never, never, void>
}
const Mongo = { context: Context.Tag<Mongo>() }
接著我們用 Effect.acquireRelease
來定義要執行的動作跟失敗的動作
const tryCreateAttachment = pipe(
MinIo.context,
Effect.flatMap(({ createAttachment, deleteAttachment }) =>
Effect.acquireRelease(
// acquire
createAttachment,
//release
(attachment, exit) =>
Exit.isFailure(exit) ? deleteAttachment(attachment) : Effect.unit
)
)
)
const tryCreateCourseRecord = (attachment: Attachment) =>
pipe(
Elastic.context,
Effect.flatMap(({ createCourseRecord, deleteCourseRecord }) =>
Effect.acquireRelease(createCourseRecord(attachment), (record, exit) =>
Exit.isFailure(exit) ? deleteCourseRecord(record) : Effect.unit
)
)
)
const tryCreateCourseMeta = (record: CourseRecord) =>
pipe(
Mongo.context,
Effect.flatMap(({ createCourseMeta, deleteCourseMeta }) =>
Effect.acquireRelease(createCourseMeta(record), (record, exit) =>
Exit.isFailure(exit) ? deleteCourseMeta(record) : Effect.unit
)
)
)
type CreateAttachment = Effect.Effect<MinIo | Scope, MinIoError, Attachment>
把滑鼠移到 createAttachment
上可以看到它的型別多了一個之前沒看過的 Scope
。這表示使用到 acquireRelease
的 effect
都需要依賴 Scope
環境來運行。
那 ... 甚麼是 Scope
呢? 我們直接從 Scope
的用法說起
effect
盒子裡面新增一個 finalizer
,來表示一連串 effect
操作結束後要執行的動作。Scope
範圍內的一連串 effect
操作結束後選擇執行 finalizer
。而說穿了,acquireRelease
就是一種特化的 Scope
,它保證只要 aquire
被執行,而且 Scope
內的各種 effect
操作也都執行完畢,release
就會被執行。實作上我們可以用以下方法把各種依賴 Scope
的 effect
組合起來。
export const tryCreateCourse = Effect.scoped(
pipe(
tryCreateAttachment,
Effect.flatMap(tryCreateCourseRecord),
Effect.flatMap(tryCreateCourseMeta)
)
)
最終就會拿到這樣的以下型別
Effect.Effect<MinIo | Elastic | Mongo, MinIoError | ElasticError | MongoError, Course>
表示我們只要集齊三個外部依賴,就可以在出錯也會復原的情況下新增課程。
最後我們再利用昨天的 Layer
技巧把上面的 effect
執行起來。
const layer = Layer.mergeAll(MinIoLayer, ElasticSearchLayer, MongoLayer)
// Layer<never, never, S3 | ElasticSearch | Database>
如何定義 Layer 昨天已經有說,今天就不再多占版面。
然後把 layer
丟給 tryCreateCourse
,最後再做個 runPromise 就能執行起來囉 !
pipe(
tryCreateCourse,
Effect.provide(layer),
Effect.either, // 轉換成 either 型別避免報錯
Effect.runPromise
)